#include <windows.h>
#include <BaseTsd.h>

#include <cerrno>
#include <fcntl.h>
#include <io.h>
#include <cstdio>
#include <cstdlib>
#include <strsafe.h>
#include <sys/stat.h>

#include "log.h"
#include "internal/LocalFS.h"

#define SEARCH_PATH_MAX_SZ (PATH_MAX + 2)

LocalFS::LocalFS()
    : m_exit(FALSE),
      m_bInitialized(FALSE),
      m_vioFsRequestBuf(nullptr),
      m_path(nullptr)
{ }

BOOL LocalFS::Initialize()
{
    if (m_bInitialized) {
        LOGW("LocalFS already initialised!");
        return TRUE;
    }

    if (!m_vioFs.Initialize()) {
        LOGE("Failed to get VioFs device");
        return FALSE;
    }

    m_path = (WCHAR*)LocalAlloc(LMEM_FIXED, sizeof(WCHAR) * SEARCH_PATH_MAX_SZ);
    if (m_path == nullptr) {
        LOGE("LocalAlloc path buffer failed with error: {}", GetLastError());
        return FALSE;
    }

    m_vioFsRequestBuf = (SubmitVioFsRequestHeader*)LocalAlloc(LMEM_FIXED, RPL_BUFF_MAX);
    if (m_vioFsRequestBuf == nullptr) {
        LocalFree(m_path);
        m_path = nullptr;
        LOGE("LocalAlloc buffer failed with error: {}", GetLastError());
        return FALSE;
    }

    m_loop = std::thread::thread(&LocalFS::HandleGuestFuseRPLLoop, this);
    m_bInitialized = TRUE;

    return TRUE;
}

LocalFS::~LocalFS()
{
    if (!m_exit) {
        m_exit = TRUE;
        m_cond.notify_one();
    }

    if (m_loop.joinable()) {
        m_loop.join();
    }

    if (m_vioFsRequestBuf != nullptr) {
        LocalFree(m_vioFsRequestBuf);
        m_vioFsRequestBuf = nullptr;
    }

    if (m_path != nullptr) {
        LocalFree(m_path);
        m_path = nullptr;
    }

    while (!m_queue.empty()) {
        m_queue.pop();
    }
}

INT LocalFS::ReadFile(int32_t fileHandle, int64_t offset, uint32_t readSize, LPVOID buffer)
{
    if (fileHandle < 0) {
        LOGE("Failed to read file with error: not opened.");
        return RETURN_ERROR;
    }

    if (_lseeki64(fileHandle, offset, SEEK_SET) == -1) {
        LOGE("Failed to seek before read error: {}", errno);
        return RETURN_ERROR;
    }

    INT nread = _read(fileHandle, buffer, readSize);
    if (nread < 0) {
        LOGE(L"Failed to read ret {}, error: {}", nread, errno);
    }

    return nread;
}

INT LocalFS::HandleGuestFuseRPLProcess(GuestFuseRPLEntry& entry, LPVOID buffer)
{
    switch (entry.opcode) {
        case GFS_REQ_READ:
            return ReadFile(entry.handle, entry.offset, entry.size, buffer);
        default:
            return RETURN_ERROR;
    }
}

void LocalFS::HandleGuestFuseRPLLoop()
{
    while (!m_exit) {
        std::unique_lock<std::mutex> lock(m_queueGuard);
        if (!m_queue.empty()) {
            GuestFuseRPLEntry entry = m_queue.front();
            m_queue.pop();
            lock.unlock();

            m_vioFsRequestBuf->rpl.hdr.magic = RPL_HDR_MAGIC;
            m_vioFsRequestBuf->rpl.hdr.opcode = entry.opcode;
            m_vioFsRequestBuf->rpl.hdr.opid = entry.opid;

            INT ret = HandleGuestFuseRPLProcess(entry, m_vioFsRequestBuf->rpl.data);
            m_vioFsRequestBuf->rpl.hdr.len = ret < 0 ? 0 : (uint32_t)ret;
            m_vioFsRequestBuf->rpl.hdr.opret = ret < 0 ? RETURN_ERROR : RETURN_SUCCESS;

            m_vioFsRequestBuf->hdr.len =
                sizeof(fuse_in_header) + sizeof(GuestFuseSrvRPLHeader) + m_vioFsRequestBuf->rpl.hdr.len;
            m_vioFsRequestBuf->hdr.opcode = GFS;
            m_vioFsRequestBuf->hdr.unique = 0;
            m_vioFsRequestBuf->hdr.nodeid = 0;
            m_vioFsRequestBuf->hdr.uid = 0;
            m_vioFsRequestBuf->hdr.gid = 0;
            m_vioFsRequestBuf->hdr.pid = 0;

            fuse_out_header readOut = { 0 };
            if (!m_vioFs.SubmitVioFsRequest(m_vioFsRequestBuf, m_vioFsRequestBuf->hdr.len,
                                            &readOut, sizeof(readOut))) {
                LOGE(L"SubmitVioFsRequest failed. Buffer length: {}, ReadOut size: {}",
                     m_vioFsRequestBuf->hdr.len, sizeof(readOut));
            }
        } else {
            m_cond.wait(lock);
        }
    }
}

void LocalFS::AddGuestFuseRPLToQueue(GuestFuseRPLEntry& entry)
{
    std::unique_lock<std::mutex> lock(m_queueGuard);

    m_queue.push(entry);
    m_cond.notify_one();
}

int LocalFS::GetFileHandle(const WCHAR* path, uint64_t hostFileHandle, const std::string& shareName)
{
    std::wstring_view psw(path);
    auto fileHandleMapIter = m_openedFileHandle.find(shareName);
    if (fileHandleMapIter == m_openedFileHandle.end())
        return RETURN_ERROR;

    auto fileHandleIter = fileHandleMapIter->second.find({path, hostFileHandle});
    if (fileHandleIter == fileHandleMapIter->second.end())
        return RETURN_ERROR;
    return fileHandleIter->second;
}

void LocalFS::AddNewFileHandle(const WCHAR* path, uint64_t hostFileHandle,
                               int localFileHandle, const std::string& shareName)
{
    std::wstring_view psw(path, wcslen(path));
    m_openedFileHandle[shareName][{psw, hostFileHandle}] = localFileHandle;
}

int LocalFS::RmFilehandle(const WCHAR* path, uint64_t hostFileHandle, const std::string& shareName)
{
    std::wstring_view psw(path);
    auto fileHandleMapIter = m_openedFileHandle.find(shareName);
    if (fileHandleMapIter == m_openedFileHandle.end())
        return RETURN_ERROR;
    auto fileHandleIter = fileHandleMapIter->second.find({path, hostFileHandle});
    if (fileHandleIter == fileHandleMapIter->second.end())
        return RETURN_ERROR;
    int fileHandle = fileHandleIter->second;
    fileHandleMapIter->second.erase(fileHandleIter);
    return fileHandle;
}

int LocalFS::CheckShareInUse(const std::string& shareName)
{
    std::lock_guard<std::mutex> guard(m_localFSGuard);

    auto fileHandleMapIter = m_openedFileHandle.find(shareName);
    if (fileHandleMapIter == m_openedFileHandle.end())
        return false;

    if (fileHandleMapIter->second.size())
        return true;

    return false;
}

void LocalFS::ShareCleanup(const std::string& shareName)
{
    std::lock_guard<std::mutex> guard(m_localFSGuard);

    auto fileHandleMapIter = m_openedFileHandle.find(shareName);
    if (fileHandleMapIter == m_openedFileHandle.end())
        return;

    for (const auto& [_, openedFileHandle] : fileHandleMapIter->second) {
        _close(openedFileHandle);
    }
    m_openedFileHandle.erase(fileHandleMapIter);
}

void LocalFS::RemoveAllFileHandle()
{
    std::lock_guard<std::mutex> guard(m_localFSGuard);

    for (auto fileHandleMapIter = m_openedFileHandle.begin();
         fileHandleMapIter != m_openedFileHandle.end();
         ++fileHandleMapIter) {
        for (const auto& [_, openedFileHandle] : fileHandleMapIter->second) {
            _close(openedFileHandle);
        }
    }

    m_openedFileHandle.clear();
}

int LocalFS::Open(const char* fullPath, GuestFuseSrvFileMetaData& fileMetaData,
                  GuestFuseSrvRPL* rpl, bool readOnly, const std::string& shareName)
{
    std::lock_guard<std::mutex> guard(m_localFSGuard);

    MultiByteToWideChar(CP_UTF8, 0, fullPath, -1, m_path, PATH_MAX);

    int oflag = (int)fileMetaData.mode;
    if (GetFileHandle(m_path, fileMetaData.hostFileHandle, shareName) > 0) {
        LOGE(L"Can't open file {}, host fileHandle {} with flags {}: already opened.",
             GET_PRINTABLE_STRING(m_path), fileMetaData.hostFileHandle, oflag);
        return RETURN_ERROR;
    }

    if (readOnly && ((oflag & _O_WRONLY) || (oflag & _O_RDWR))) {
        LOGE(L"Can't open file {}, host fileHandle {} with flags {}: share is read only.",
             GET_PRINTABLE_STRING(m_path), fileMetaData.hostFileHandle, oflag);
        return -EACCES;
    }

    int pmode = _S_IREAD |
        ((oflag & _O_WRONLY) ? _S_IWRITE : 0) |
        ((oflag & _O_RDWR) ? (_S_IREAD | _S_IWRITE) : 0);

    int fileHandle;
    _wsopen_s(&fileHandle, m_path, oflag, _SH_DENYNO, pmode);
    if (fileHandle < 0) {
        LOGE(L"Can't open file {}, with flags {}, error: {}", GET_PRINTABLE_STRING(m_path), oflag, errno);
        return RETURN_ERROR;
    }
    AddNewFileHandle(m_path, fileMetaData.hostFileHandle, fileHandle, shareName);
    return RETURN_SUCCESS;
}

int LocalFS::Release(const char* fullPath, GuestFuseSrvFileMetaData& fileMetaData,
                     GuestFuseSrvRPL* rpl, const std::string& shareName)
{
    std::lock_guard<std::mutex> guard(m_localFSGuard);

    MultiByteToWideChar(CP_UTF8, 0, fullPath, -1, m_path, PATH_MAX);

    int oflag = (int)fileMetaData.mode;
    int fileHandle = RmFilehandle(m_path, fileMetaData.hostFileHandle, shareName);
    if (fileHandle < 0) {
        LOGE(L"Can't release file {}, host fileHandle {}: already released.",
             GET_PRINTABLE_STRING(m_path), fileMetaData.hostFileHandle);
        return RETURN_ERROR;
    }

    int ret = _close(fileHandle);
    if (ret) {
        LOGE(L"Can't close file {}, host fileHandle {}, guest fileHandle {}, error: {}",
             GET_PRINTABLE_STRING(m_path), fileMetaData.hostFileHandle, fileHandle, errno);
    }

    return RETURN_SUCCESS;
}

int LocalFS::GetAttr(const char* fullPath, GuestFuseSrvRPL* rpl)
{
    GuestFuseSrvFileMetaData* md = &rpl->file[0].md;
    struct _stat64 st;
    int ret;

    rpl->hdr.len = sizeof(GuestFuseSrvFileMetaData);

    std::lock_guard<std::mutex> guard(m_localFSGuard);
    MultiByteToWideChar(CP_UTF8, 0, fullPath, -1, m_path, PATH_MAX);

    ret = _wstat64(m_path, &st);
    if (ret != 0) {
        LOGE(L"Failed to get attr of {}, error: {}", GET_PRINTABLE_STRING(m_path), errno);
        return RETURN_ERROR;
    }

    md->mode = st.st_mode;
    md->size = st.st_size;
    md->atime = st.st_atime;
    md->mtime = st.st_mtime;
    md->ctime = st.st_ctime;
    md->pathLen = 0;
    md->padLen = 0;

    return RETURN_SUCCESS;
}

int LocalFS::Read(const char* fullPath, GuestFuseSrvFileMetaData& fileMetaData,
                  GuestFuseSrvRPL* rpl, const std::string& shareName)
{
    uint32_t readSize = (uint32_t)(fileMetaData.size > RPL_READ_MAX ? RPL_READ_MAX : fileMetaData.size);

    std::lock_guard<std::mutex> guard(m_localFSGuard);
    MultiByteToWideChar(CP_UTF8, 0, fullPath, -1, m_path, PATH_MAX);

    int fileHandle = GetFileHandle(m_path, fileMetaData.hostFileHandle, shareName);
    if (fileHandle < 0) {
        LOGE(L"Can't read file {}, host fileHandle {}, error: not opened.",
             GET_PRINTABLE_STRING(m_path), fileMetaData.hostFileHandle);
    }

    GuestFuseRPLEntry entry = { 0 };
    entry.opcode = rpl->hdr.opcode;
    entry.opid = rpl->hdr.opid;
    entry.handle = fileHandle;
    entry.size = readSize;
    entry.offset = fileMetaData.offset;

    AddGuestFuseRPLToQueue(entry);

    return RETURN_SUCCESS;
}

int LocalFS::Write(const char* fullPath, GuestFuseSrvFileMetaData& fileMetaData,
                   const char* data, GuestFuseSrvRPL* rpl, const std::string& shareName)
{
    rpl->hdr.len = sizeof(GuestFuseSrvFileMetaData);

    std::lock_guard<std::mutex> guard(m_localFSGuard);
    MultiByteToWideChar(CP_UTF8, 0, fullPath, -1, m_path, PATH_MAX);

    int fileHandle = GetFileHandle(m_path, fileMetaData.hostFileHandle, shareName);
    if (fileHandle < 0) {
        LOGE(L"Can't write to file {}, host fileHandle {}, error: not opened.",
             GET_PRINTABLE_STRING(m_path), fileMetaData.hostFileHandle);
        return RETURN_ERROR;
    }

    if (_lseeki64(fileHandle, fileMetaData.offset, SEEK_SET) == -1) {
        LOGE(L"File {}, host fileHandle {}, seek before write error: {}",
             GET_PRINTABLE_STRING(m_path), fileMetaData.hostFileHandle, errno);
        return RETURN_ERROR;
    }
    int nwrite = _write(fileHandle, data, (unsigned int)fileMetaData.size);
    if (nwrite < 0) {
        LOGE(L"File {}, host fileHandle {}, write ret {}, error: {}",
             GET_PRINTABLE_STRING(m_path), fileMetaData.hostFileHandle, nwrite, errno);
        return RETURN_ERROR;
    }

    GuestFuseSrvFileMetaData* md = &rpl->file[0].md;
    md->size = nwrite;

    return RETURN_SUCCESS;
}

int LocalFS::ReadDir(const char* fullPath, GuestFuseSrvRPL* rpl, int rplBuffSz)
{
    WIN32_FIND_DATAW findFileData;
    uint8_t* rplStart = (uint8_t*)rpl;
    uint8_t* rplData = (uint8_t*)&rpl->file[0];
    GuestFuseSrvFile* fuseSrvFile = (GuestFuseSrvFile*)rplData;

    ZeroMemory(&fuseSrvFile->md, sizeof(fuseSrvFile->md));

    std::lock_guard<std::mutex> guard(m_localFSGuard);

    MultiByteToWideChar(CP_UTF8, 0, fullPath, -1, m_path, PATH_MAX);
    StringCchCatW(m_path, SEARCH_PATH_MAX_SZ, TEXT("\\*"));

    HANDLE hFind = FindFirstFileW(m_path, &findFileData);
    if (INVALID_HANDLE_VALUE == hFind) {
        LOGE(L"Unable to find first file in dir: {}, error: {}", GET_PRINTABLE_STRING(m_path), GetLastError());
        return RETURN_ERROR;
    }

    if ((rplData - rplStart + sizeof(fuseSrvFile->md) + PATH_MAX) > rplBuffSz) {
        LOGE("ReadDir reply buffer too small: {}", rplBuffSz);
        rpl->hdr.len = 0;
        return RETURN_ERROR;
    }

    int nbytes = WideCharToMultiByte(CP_UTF8, 0, findFileData.cFileName, -1, fuseSrvFile->path, PATH_MAX, NULL, NULL);
    if (!nbytes) {
        LOGE(L"ReadDir cannot convert to multibyte: {}", GET_PRINTABLE_STRING(findFileData.cFileName));
        return RETURN_ERROR;
    }
    fuseSrvFile->md.pathLen = nbytes;

    while (1) {
        fuseSrvFile->md.padLen = RPL_PATH_ALIGN - (fuseSrvFile->md.pathLen & (RPL_PATH_ALIGN - 1));
        uint32_t entry_len =
            (uint32_t)sizeof(fuseSrvFile->md) + fuseSrvFile->md.pathLen + fuseSrvFile->md.padLen;
        rpl->hdr.len += entry_len;
        rplData += entry_len;
        fuseSrvFile = (GuestFuseSrvFile*)rplData;

        ZeroMemory(&fuseSrvFile->md, sizeof(fuseSrvFile->md));

        if (FindNextFileW(hFind, &findFileData) != 0) {
            if ((rplData - rplStart + sizeof(fuseSrvFile->md) + PATH_MAX) > rplBuffSz) {
                LOGE("ReadDir reply buffer too small: {}", rplBuffSz);
                rpl->hdr.len = 0;
                return RETURN_ERROR;
            }

            nbytes = WideCharToMultiByte(CP_UTF8, 0, findFileData.cFileName, -1,
                                         fuseSrvFile->path, PATH_MAX, NULL, NULL);
            if (!nbytes) {
                LOGE(L"ReadDir cannot convert to multibyte: {}", GET_PRINTABLE_STRING(findFileData.cFileName));
                return RETURN_ERROR;
            }
            fuseSrvFile->md.pathLen = nbytes;
        } else {
            DWORD err = GetLastError();
            if (err != ERROR_NO_MORE_FILES) {
                LOGE("FindNextFile failed with error: {}", err);
            }
            FindClose(hFind);
            break;
        }
    }
    return RETURN_SUCCESS;
}

int LocalFS::Rename(const char* fullPath, const char* newFullPath)
{
    std::lock_guard<std::mutex> guard(m_localFSGuard);
    MultiByteToWideChar(CP_UTF8, 0, fullPath, -1, m_path, PATH_MAX);
    WCHAR new_path[PATH_MAX];
    MultiByteToWideChar(CP_UTF8, 0, newFullPath, -1, new_path, PATH_MAX);

    int res = MoveFile(m_path, new_path);
    if (!res) {
        LOGE(L"Cannot rename file {} to {}, error: {}",
             GET_PRINTABLE_STRING(m_path), GET_PRINTABLE_STRING(new_path), GetLastError());
        return RETURN_ERROR;
    }

    return RETURN_SUCCESS;
}

int LocalFS::Remove(const char* fullPath)
{
    std::lock_guard<std::mutex> guard(m_localFSGuard);

    MultiByteToWideChar(CP_UTF8, 0, fullPath, -1, m_path, PATH_MAX);

    bool ret = DeleteFile(m_path);
    if (!ret) {
        int err = GetLastError();
        LOGE(L"Can't remove file {}, error: {}", GET_PRINTABLE_STRING(m_path), GetLastError());
        if (err == ERROR_FILE_NOT_FOUND)
            return -ENOENT;
        if (err == ERROR_ACCESS_DENIED)
            return -EACCES;
        return RETURN_ERROR;
    }

    return RETURN_SUCCESS;
}

int LocalFS::Truncate(const char* fullPath, GuestFuseSrvFileMetaData& fileMetaData)
{
    std::lock_guard<std::mutex> guard(m_localFSGuard);

    MultiByteToWideChar(CP_UTF8, 0, fullPath, -1, m_path, PATH_MAX);

    int fileHandle;
    _wsopen_s(&fileHandle, m_path, _O_BINARY | _O_WRONLY, _SH_DENYNO, _S_IREAD | _S_IWRITE);
    if (fileHandle < 0) {
        LOGE(L"Can't open file {} for truncation error: {}", GET_PRINTABLE_STRING(m_path), errno);
        return -errno;
    }
    if (_chsize(fileHandle, (long)fileMetaData.offset)) {
        LOGE(L"Can't change size of file {}, error: {}", GET_PRINTABLE_STRING(m_path), errno);
        return -errno;
    }
    if (_close(fileHandle)) {
        LOGE(L"Can't close truncated file {}, error: {}", GET_PRINTABLE_STRING(m_path), errno);
        return -errno;
    }

    return RETURN_SUCCESS;
}

int LocalFS::MkDir(const char* fullPath)
{
    std::lock_guard<std::mutex> guard(m_localFSGuard);

    MultiByteToWideChar(CP_UTF8, 0, fullPath, -1, m_path, PATH_MAX);

    if (_wmkdir(m_path)) {
        LOGE(L"Can't mkdir {}, error: {}", GET_PRINTABLE_STRING(m_path), errno);
        return -errno;
    }
    return RETURN_SUCCESS;
}

int LocalFS::RmDir(const char* fullPath)
{
    std::lock_guard<std::mutex> guard(m_localFSGuard);

    MultiByteToWideChar(CP_UTF8, 0, fullPath, -1, m_path, PATH_MAX);

    if (_wrmdir(m_path)) {
        LOGE(L"Can't rmdir {}, error: {}", GET_PRINTABLE_STRING(m_path), errno);
        return -errno;
    }

    return RETURN_SUCCESS;
}
